/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Haystack Software Inc. All rights reserved.
 *  Licensed under the PolyForm Strict License 1.0.0. See License.txt in the project root for
 *  license information.
 *--------------------------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See code-license.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { Schemas } from "vs/base/common/network"
import { OperatingSystem } from "vs/base/common/platform"
import { URI } from "vs/base/common/uri"
import { ICommandService } from "vs/platform/commands/common/commands"
import { ITextEditorSelection } from "vs/platform/editor/common/editor"
import { IFileService } from "vs/platform/files/common/files"
import { IInstantiationService } from "vs/platform/instantiation/common/instantiation"
import { IOpenerService } from "vs/platform/opener/common/opener"
import { IQuickInputService } from "vs/platform/quickinput/common/quickInput"
import { IWorkspaceContextService } from "vs/platform/workspace/common/workspace"
import {
  ITerminalLinkOpener,
  ITerminalSimpleLink,
} from "vs/workbench/contrib/terminalContrib/links/browser/links"
import {
  osPathModule,
  updateLinkWithRelativeCwd,
} from "vs/workbench/contrib/terminalContrib/links/browser/terminalLinkHelpers"
import {
  ITerminalCapabilityStore,
  TerminalCapability,
} from "vs/platform/terminal/common/capabilities/capabilities"
import { IWorkbenchEnvironmentService } from "vs/workbench/services/environment/common/environmentService"
import { IHostService } from "vs/workbench/services/host/browser/host"
import { QueryBuilder } from "vs/workbench/services/search/common/queryBuilder"
import { ISearchService } from "vs/workbench/services/search/common/search"
import { IConfigurationService } from "vs/platform/configuration/common/configuration"
import {
  detectLinks,
  getLinkSuffix,
} from "vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing"
import { ITerminalLogService } from "vs/platform/terminal/common/terminal"
import { IHaystackService } from "vs/workbench/services/haystack/common/haystackService"
import { IRange } from "vs/editor/common/core/range"

export class TerminalLocalFileLinkOpener implements ITerminalLinkOpener {
  constructor(
    @IHaystackService private readonly _haystackService: IHaystackService
  ) {}

  async open(link: ITerminalSimpleLink): Promise<void> {
    if (!link.uri) {
      throw new Error("Tried to open file link without a resolved URI")
    }
    const linkSuffix = link.parsedLink
      ? link.parsedLink.suffix
      : getLinkSuffix(link.text)
    let selection: ITextEditorSelection | undefined = link.selection
    if (!selection) {
      selection =
        linkSuffix?.row === undefined
          ? undefined
          : {
              startLineNumber: linkSuffix.row ?? 1,
              startColumn: linkSuffix.col ?? 1,
              endLineNumber: linkSuffix.rowEnd,
              endColumn: linkSuffix.colEnd,
            }
    }
    await this._haystackService.createFileEditor(
      link.uri,
      {
        selectionRange: selection
          ? ({
              startLineNumber: selection.startLineNumber,
              startColumn: selection.startColumn,
              endLineNumber: selection.endLineNumber,
              endColumn: selection.endColumn,
            } as IRange)
          : undefined,
      },
      {
        pinned: true,
        revealIfOpened: true,
      }
    )
  }
}

export class TerminalLocalFolderInWorkspaceLinkOpener
  implements ITerminalLinkOpener
{
  constructor(
    @ICommandService private readonly _commandService: ICommandService
  ) {}

  async open(link: ITerminalSimpleLink): Promise<void> {
    if (!link.uri) {
      throw new Error(
        "Tried to open folder in workspace link without a resolved URI"
      )
    }
    await this._commandService.executeCommand("revealInExplorer", link.uri)
  }
}

export class TerminalLocalFolderOutsideWorkspaceLinkOpener
  implements ITerminalLinkOpener
{
  constructor(@IHostService private readonly _hostService: IHostService) {}

  async open(link: ITerminalSimpleLink): Promise<void> {
    if (!link.uri) {
      throw new Error(
        "Tried to open folder in workspace link without a resolved URI"
      )
    }
    this._hostService.openWindow([{ folderUri: link.uri }], {
      forceNewWindow: true,
    })
  }
}

export class TerminalSearchLinkOpener implements ITerminalLinkOpener {
  protected _fileQueryBuilder =
    this._instantiationService.createInstance(QueryBuilder)

  constructor(
    private readonly _capabilities: ITerminalCapabilityStore,
    private readonly _initialCwd: string,
    private readonly _localFileOpener: TerminalLocalFileLinkOpener,
    private readonly _localFolderInWorkspaceOpener: TerminalLocalFolderInWorkspaceLinkOpener,
    private readonly _getOS: () => OperatingSystem,
    @IFileService private readonly _fileService: IFileService,
    @IInstantiationService
    private readonly _instantiationService: IInstantiationService,
    @ITerminalLogService private readonly _logService: ITerminalLogService,
    @IQuickInputService private readonly _quickInputService: IQuickInputService,
    @ISearchService private readonly _searchService: ISearchService,
    @IWorkspaceContextService
    private readonly _workspaceContextService: IWorkspaceContextService,
    @IWorkbenchEnvironmentService
    private readonly _workbenchEnvironmentService: IWorkbenchEnvironmentService
  ) {}

  async open(link: ITerminalSimpleLink): Promise<void> {
    const osPath = osPathModule(this._getOS())
    const pathSeparator = osPath.sep

    // Remove file:/// and any leading ./ or ../ since quick access doesn't understand that format
    let text = link.text.replace(/^file:\/\/\/?/, "")
    text = osPath.normalize(text).replace(/^(\.+[\\/])+/, "")

    // Try extract any trailing line and column numbers by matching the text against parsed
    // links. This will give a search link `foo` on a line like `"foo", line 10` to open the
    // quick pick with `foo:10` as the contents.
    if (link.contextLine) {
      const parsedLinks = detectLinks(link.contextLine, this._getOS())
      const matchingParsedLink = parsedLinks.find(
        (parsedLink) => parsedLink.suffix && link.text === parsedLink.path.text
      )
      if (matchingParsedLink) {
        if (matchingParsedLink.suffix?.row !== undefined) {
          text += `:${matchingParsedLink.suffix.row}`
          if (matchingParsedLink.suffix?.col !== undefined) {
            text += `:${matchingParsedLink.suffix.col}`
          }
        }
      }
    }

    // Remove `:<one or more non number characters>` from the end of the link.
    // Examples:
    // - Ruby stack traces: <link>:in ...
    // - Grep output: <link>:<result line>
    // This only happens when the colon is _not_ followed by a forward- or back-slash as that
    // would break absolute Windows paths (eg. `C:/Users/...`).
    text = text.replace(/:[^\\/\d][^\d]*$/, "")

    // Remove any trailing periods after the line/column numbers, to prevent breaking the search feature, #200257
    // Examples:
    // "Check your code Test.tsx:12:45." -> Test.tsx:12:45
    // "Check your code Test.tsx:12." -> Test.tsx:12

    text = text.replace(/\.$/, "")

    // If any of the names of the folders in the workspace matches
    // a prefix of the link, remove that prefix and continue
    this._workspaceContextService.getWorkspace().folders.forEach((folder) => {
      if (
        text.substring(0, folder.name.length + 1) ===
        folder.name + pathSeparator
      ) {
        text = text.substring(folder.name.length + 1)
        return
      }
    })
    let cwdResolvedText = text
    if (this._capabilities.has(TerminalCapability.CommandDetection)) {
      cwdResolvedText =
        updateLinkWithRelativeCwd(
          this._capabilities,
          link.bufferRange.start.y,
          text,
          osPath,
          this._logService
        )?.[0] || text
    }

    // Try open the cwd resolved link first
    if (await this._tryOpenExactLink(cwdResolvedText, link)) {
      return
    }

    // If the cwd resolved text didn't match, try find the link without the cwd resolved, for
    // example when a command prints paths in a sub-directory of the current cwd
    if (text !== cwdResolvedText) {
      if (await this._tryOpenExactLink(text, link)) {
        return
      }
    }

    // Fallback to searching quick access
    return this._quickInputService.quickAccess.show(text)
  }

  private async _getExactMatch(
    sanitizedLink: string
  ): Promise<IResourceMatch | undefined> {
    // Make the link relative to the cwd if it isn't absolute
    const os = this._getOS()
    const pathModule = osPathModule(os)
    const isAbsolute = pathModule.isAbsolute(sanitizedLink)
    let absolutePath: string | undefined = isAbsolute
      ? sanitizedLink
      : undefined
    if (!isAbsolute && this._initialCwd.length > 0) {
      absolutePath = pathModule.join(this._initialCwd, sanitizedLink)
    }

    // Try open as an absolute link
    let resourceMatch: IResourceMatch | undefined
    if (absolutePath) {
      let normalizedAbsolutePath: string = absolutePath
      if (os === OperatingSystem.Windows) {
        normalizedAbsolutePath = absolutePath.replace(/\\/g, "/")
        if (normalizedAbsolutePath.match(/[a-z]:/i)) {
          normalizedAbsolutePath = `/${normalizedAbsolutePath}`
        }
      }
      let uri: URI
      if (this._workbenchEnvironmentService.remoteAuthority) {
        uri = URI.from({
          scheme: Schemas.vscodeRemote,
          authority: this._workbenchEnvironmentService.remoteAuthority,
          path: normalizedAbsolutePath,
        })
      } else {
        uri = URI.file(normalizedAbsolutePath)
      }
      try {
        const fileStat = await this._fileService.stat(uri)
        resourceMatch = { uri, isDirectory: fileStat.isDirectory }
      } catch {
        // File or dir doesn't exist, continue on
      }
    }

    // Search the workspace if an exact match based on the absolute path was not found
    if (!resourceMatch) {
      const results = await this._searchService.fileSearch(
        this._fileQueryBuilder.file(
          this._workspaceContextService.getWorkspace().folders,
          {
            filePattern: sanitizedLink,
            maxResults: 2,
          }
        )
      )
      if (results.results.length > 0) {
        if (results.results.length === 1) {
          // If there's exactly 1 search result, return it regardless of whether it's
          // exact or partial.
          resourceMatch = { uri: results.results[0].resource }
        } else if (!isAbsolute) {
          // For non-absolute links, exact link matching is allowed only if there is a single an exact
          // file match. For example searching for `foo.txt` when there is no cwd information
          // available (ie. only the initial cwd) should open the file directly only if there is a
          // single file names `foo.txt` anywhere within the folder. These same rules apply to
          // relative paths with folders such as `src/foo.txt`.
          const results = await this._searchService.fileSearch(
            this._fileQueryBuilder.file(
              this._workspaceContextService.getWorkspace().folders,
              {
                filePattern: `**/${sanitizedLink}`,
              }
            )
          )
          // Find an exact match if it exists
          const exactMatches = results.results.filter((e) =>
            e.resource.toString().endsWith(sanitizedLink)
          )
          if (exactMatches.length === 1) {
            resourceMatch = { uri: exactMatches[0].resource }
          }
        }
      }
    }
    return resourceMatch
  }

  private async _tryOpenExactLink(
    text: string,
    link: ITerminalSimpleLink
  ): Promise<boolean> {
    const sanitizedLink = text.replace(/:\d+(:\d+)?$/, "")
    try {
      const result = await this._getExactMatch(sanitizedLink)
      if (result) {
        const { uri, isDirectory } = result
        const linkToOpen = {
          // Use the absolute URI's path here so the optional line/col get detected
          text: result.uri.path + (text.match(/:\d+(:\d+)?$/)?.[0] || ""),
          uri,
          bufferRange: link.bufferRange,
          type: link.type,
        }
        if (uri) {
          await (isDirectory
            ? this._localFolderInWorkspaceOpener.open(linkToOpen)
            : this._localFileOpener.open(linkToOpen))
          return true
        }
      }
    } catch {
      return false
    }
    return false
  }
}

interface IResourceMatch {
  uri: URI
  isDirectory?: boolean
}

export class TerminalUrlLinkOpener implements ITerminalLinkOpener {
  constructor(
    private readonly _isRemote: boolean,
    @IOpenerService private readonly _openerService: IOpenerService,
    @IConfigurationService
    private readonly _configurationService: IConfigurationService
  ) {}

  async open(link: ITerminalSimpleLink): Promise<void> {
    if (!link.uri) {
      throw new Error("Tried to open a url without a resolved URI")
    }
    // It's important to use the raw string value here to avoid converting pre-encoded values
    // from the URL like `%2B` -> `+`.
    this._openerService.open(link.text, {
      allowTunneling:
        this._isRemote &&
        this._configurationService.getValue("remote.forwardOnOpen"),
      allowContributedOpeners: true,
      openExternal: true,
    })
  }
}
